-
-
Notifications
You must be signed in to change notification settings - Fork 223
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fluent integration #211
Fluent integration #211
Conversation
Here's a rendered draft of what I want the final API to look like (from the docs I'm writing). This API is a little magical but I think it should end up nice to use. Thoughts? Internationalization with FluentEnabling the Askama's i18n relies on Fluent to parse and apply translations. Using i18n tries to be as simple as possible, but it's still a little involved. Create a folder structure at your project root, organized like so:
Add some localizations in Fluent's
hello = Hello, $name!
age.tracker = You are $age_hours hours old.
hello = ¡Hola, $name!
age.tracker = Tiene $age_hours horas. Call the
This will create a module Now, on templates you want to localize, add a member
And now you can use the <h1>{ localize(hello, name: name) }</h1>
<h3>{ localize(age.tracker, age_hours: age_hours) }</h1> (TODO: should we automatically pass eligible template variables Now, your template will be automatically translated when rendered:
<h1>Hello, Jamie!</h1>
<h3>You are 195,481.8 hours old.</h1>
<h1>¡Hola, Jamie!</h1>
<h3>Tienes 195.481,8 horas.</h1> It's up to you to pass the correct locale in for each user. |
Thanks for working on this! Notes so far:
|
Glad to, it's a fun little project :)
|
The question about how template implementations will find where the globals are defined is a real issue, though. Have you thought about how that might work? I don't think we want an extra argument to If Fluent requires a Let's stay away from |
I was just thinking the
There's definitely some lack of separation of concerns in here, 🤷♀️ what can you do.
Yeah that's a problem inherent in the fluent API, it's very allocation-y. Also doesn't take a |
Ok a few more thoughts on how to find the translations: What if We could also just make a macro you call in your |
Yeah, I quite like the idea of letting the filter look into an Fluent returning |
Was wondering this morning if we could do a custom derive instead of the other macro thing? So: #[derive(Localization)]
struct I18n; And then all the generated stuff would be |
Or, really next-level, we could route localized template rendering through that interface (so |
What do you think about this? fn main() {
let hello = HelloTemplate { name: "world" }; // instantiate your struct
println!("{}", hello.locale("en-US").render().unwrap()); // then render it in english
} You can import the |
We could make it a trait and the codegen would be pretty much the same; there need to be some One new idea I had was having a
That makes it clear that that particular struct member is "special", and is actually pretty extensible if we ever wanna pass other stuff into templates, sorta halfway to a builder pattern.
|
random thought: @djc, how would you feel about a short syntax for localization? it's already something the parser knows about anyway. like instead of |
I made it so that your build fails if your localizations have syntax errors. Could also make it just a warning, fluent degrades gracefully. another fun thing: you can now run
|
So I think I want to require that the user gives the global object an explicit name in some way -- though I'm not yet sure what the best way to go about that is. And then I also want localized template rendering to explicitly pull in both the global and the locale name. Within those constraints I'm not yet sure what the best way to do it might be. I do like the idea of dedicating some specific syntax to localized expressions, but I think we should punt on that for this PR and first get the basics working with the |
I apologize if I'm late to the party, but would a typed approach work instead of a filter? I understand if people prefer to have the filter syntax settled; but if not, below is what I'm thinking: Accessing fluent definitions from RustFluent message is a struct with any possible variables as fields. An enum with the available locales would also be generated, implementing
Generated // Generated code should be edition 2015 compatible
use ::askama::Localize;
#[allow(non_camel_case_types)]
pub enum Locales {
enUS,
esMX,
}
impl From<Box<str>> for Locales {
fn from(s: Box<str>) -> Locales {
match s.as_ref() {
"en-US" => Locales::enUS,
"es-MX" => Locales::esMX,
other => panic!("{} is not a locale in your i18n directory.", other),
}
}
}
pub struct Hello<'a> {
pub name: &'a str,
}
impl<'a> Localize for Hello<'a> {
type Locales = Locales;
fn to_locale(&self, locale: Self::Locales) -> String {
match locale {
Locales::enUS => format!("Hello, {name}", name=self.name),
Locales::esMX => format!("¡Hola, {name}", name=self.name),
}
}
}
pub struct AgeTracker<'a> {
pub age_hours: &'a str,
}
impl<'a> Localize for AgeTracker<'a> {
type Locales = Locales;
fn to_locale(&self, locale: Self::Locales) -> String {
match locale {
Locales::enUS => format!("You are {age_hours} hours old", age_hours=self.age_hours),
Locales::esMX => format!("Tiene {age_hours} horas", age_hours=self.age_hours)
}
}
} You'd then use localizations like so in your template contexts:
<h2>{ hello }</h2>
<h6>{ age_tracker }</h6>
use askama::Template;
use crate::i18n::{Locales, Hello, AgeTracker};
#[derive(Template)]
#[template(path="greeting.html")]
pub struct Greeting<'a> {
hello: Hello<'a>
age_tracker: AgeTracker<'a>
}
#[test]
fn localization_works() {
let expected_english = "<h2>Hello, Alice</h2>
<h6>You are 157680 hours old</h6>";
let expected_spanish = "<h2>¡Hola, Alice</h2>
<h6>Tiene 157680 horas</h6>";
let greeting = Greeting {
name: Alice,
age_tracker: 157680,
};
// let english = greeting.with_locale("en-US").render().unwrap() will also work
let english = greeting.with_locale(Locales::enUS).render().unwrap();
let spanish = greeting.with_locale(Locales::esMX).render().unwrap():
assert_eq!(&english, expected_english);
assert_eq!(&spanish, expected_spanish);
} This uses the builder-pattern mentioned by @kazimuth in this comment. The Where to put the generated i18n codeUsing something like I hope this doesn't wrench the conversation off-topic too much. I appreciate all of the work you're putting into this, @kazimuth! |
It looks like you're only able to translate text in the structs for that. The problem with that is that not all text is contained within the struct. Plenty of 'hard-coded' text in the html files also needs a way to call out for localization. |
@Deedasmi I must be missing something. It seems that the point of Fluent is to have all translated text in these definition files. Each "phrase" would then be translated into a Rust data structure. If it's a string literal, then you could use an empty struct that implements |
I could also very easily be missing something. But the rest of the conversation ties the locale to the template, which would allow things like |
I mostly proposed the idea because conceptually it just seems so cool, but the ergonomics probably aren't there, at least as far as I can figure out on my own 😜 |
Sounds very cool! Sorry I haven't been as active here recently, it's very busy. Please rest assured that I'm still interested and will review this more closely as it gets closer to being mergeable. |
This is pretty much ready to merge. Current cargo docs: Internationalization with FluentEnabling the Askama's i18n relies on Fluent to parse and apply translations. Using i18n tries to be as simple as possible, but it's still a little involved. Create a folder structure at your project root, organized like so:
Add some localizations in Fluent's
hello = Hello, $name!
age.tracker = You are $age_hours hours old.
hello = ¡Hola, $name!
age.tracker = Tiene $age_hours horas. Call the extern crate askama;
use askama::{Localize, impl_localize};
impl_localize! {
#[localize(path = "i18n", default_locale = "en_US")]
pub struct AppLocalizer(_);
} This creates a struct called This will bake translations you provide into the output executable, to ease (Note: tests will be autogenerated to ensure that Now, on templates you want to localize, add a member of type #[derive(Template)]
#[template(path = "hello.html")]
struct HelloTemplate<'a> {
#[localizer]
localizer: AppLocalizer,
name: &'a str,
age_hours: f32,
} And now you can use the <h1>{{ localize(hello, name: name) }}</h1>
<h3>{{ localize(age.tracker, age_hours: age_hours) }}</h1> Now, your template will be automatically translated when rendered: println!("{}", HelloTemplate {
localizer: AppLocalizer::new(Some("en_US"), None),
name: "Jamie",
age_hours: 195_481.8
});
/*
<h1>Hello, Jamie!</h1>
<h3>You are 195,481.8 hours old.</h1>
*/
println!("{}", HelloTemplate {
localizer: AppLocalizer::new(Some("es_MX"), None),
name: "Jamie",
age_hours: 195_481.8
});
/*
<h1>¡Hola, Jamie!</h1>
<h3>Tienes 195.481,8 horas.You are 195,481.8 hours old.</h1>
*/ To generate a coverage report, run: This will report on the percent of messages translated in each locale. |
Ping @djc |
Hey @djc, i think this is ready to merge. Are there more changes you want me to make? |
Sorry - I've just been super busy. It's not forgotten and near the top of my OSS TODO list. Should've let you know sooner... |
no prob, i totally understand :) just let me know |
So I started taking a look at this, but this will need a bunch more work for me to be able to really review this. I hope you're up for that? I'm really sorry it's taken so long, but the size and complexity of this has turned this review into a chore for which I need to really gather time and energy and priority. I prefer to review commit-by-commit and ideally want as-small-as-possible logically atomic commits. I'm not yet sure what a logical sequencing might be? I noticed some commits that change the parser (without adding anything), maybe we should start with that, in a separate PR? The hardest part with reviewing this PR currently is also the changes to earlier commits in later commits, so maybe some squashing could already help. Maybe we could take the big Also it would help if you can rebase this on master rather than having a merge commit in there. |
No problem, I totally understand! I can rebase this and split it up to be more comprehensible. There should be some opportunities to simplify + add tests and docs, as well. The basic chunks:
I can do these in whatever order. Should I open separate PRs for them, or make a chain of commits in a single PR? |
Closing in favor of another PR. |
One PR per chunk might save you on rebasing, so let's do that? Also given my track record dealing with this large PR, let's go the safe route of multiple smaller ones... |
Starting the work discussed in #202, not functional in any way yet.
My current implementation will end up baking the compiled templates into result executables to keep deployment simple, although I think I'll also add in the ability to reload translation files at runtime somehow.